1. Introduction > 2. Mon premier projet > 3. Modèle > 4. Vue > 5. Liste de données > 6. Mapping objet/relationnel > 7. Contrôleurs > 8. Application > 9. Personnalisation

Chapitre 6. Mapping objet-relationnel

Le mapping objet-relationnel vous permet de définir dans quelles tables et colonnes de votre base de données l'état de votre entité sera sauvegardée.
Les outils objet-relationnel vous permettent de travailler avec des objets plutôt qu'avec des tables et des colonnes et génèrent automatiquement le code SQL pour lire et mettre à jour la base de données. De cette manière, vous n'avez pas besoin d'un accès direct à la base de données SQL. Bien sûr, vous devez définir précisément la manière dont vos classes correspondent aux tables et ce travail est effectué à l'aide des annotations JPA. Les entités OpenXava sont des entités JPA, c'est pourquoi la persistance dans OpenXava est prise en charge par le Java Persistance API (JPA). Ce chapitre montre les techniques de base de mise en correspondance ainsi que quelques cas spéciaux. Si vous souhaitez en savoir plus sur JPA , consultez la documentation de Hibernate Annotations (l'implémentation JPA utilisée par défaut par OpenXava) ou tout autre manuel JPA que vous souhaitez.

Mapping d'entité

L'annotation @Table spécifie la table principale pour l'entité annotée. Des tables additionnelles peuvent être ajoutées avec les annotations @SecondaryTable ou @SecondaryTables. Si aucune annotation @Table n'est présente, une valeur par défaut sera appliquée. Exemple :
@Entity
@Table(name="CUST", schema="XAVATEST")
public class Customer {

Mapping de propriété

L'annotation @Column est utilisée pour spécifier la colonne d'une propriété persistante ou champ. Si aucune annotation @Column est écrite, une valeur par défaut est appliquée. Une simple exemple :
@Column(name="DESC", length=512)
private String description;
Voici un exemple en spécifiant l'annotation pour une méthode getter :
@Column(name="DESC", nullable=false, length=512)
public String getDescription() { return description; }
D'autres exemples :
@Column(name="DESC",
 columnDefinition="CLOB NOT NULL",
 table="EMP_DETAIL")
@Lob
private String description;
 
@Column(name="ORDER_COST", updatable=false, precision=12, scale=2)
private BigDecimal cost;

Mapping de référence

L'annotation @JoinColumn est utilisée pour spécifier une colonne représentant une référence. Exemple :
@ManyToOne
@JoinColumn(name="CUST_ID")
private Customer customer;
Si vous avez besoin de définir un mapping pour des clés étrangères composites, il faut employer l'annotation @JoinColumns. Cette annotation groupe plusieurs @JoinColumn pour la même référence. Lorsque l'annotation @JoinColumns est utilisée, les deux attributs name et referencedColumnName doivent être définis dans chaque annotation @JoinColumn. Exemple :
@ManyToOne
@JoinColumns({
 @JoinColumn(name="INV_YEAR", referencedColumnName="YEAR"),
 @JoinColumn(name="INV_NUMBER", referencedColumnName="NUMBER")
})
private Invoice invoice;

Mapping de collection

Lorsque vous utilisez l'annotation @OneToMany pour une collection, le mapping dépende de la référence utilisée dans l'autre partie de l'association, c'est-à-dire que, généralement, il n'y a rien besoin de faire. Mais si vous utilisez l'annotation @ManyToMany, il est utile de spécifier la table de jointure avec l'annotation @JoinTable comme ceci :
@ManyToMany
@JoinTable(name="CUSTOMER_STATE",
 joinColumns=@JoinColumn(name="CUSTOMER"),
 inverseJoinColumns=@JoinColumn(name="STATE")
)
private Collection<State> states;
Si l'annotation @JoinTable manque, des valeurs par défaut sont appliquées

Please, translate to French: When you use @ElementCollection (new in v5.0) for a collection you can use @CollectionTable and @AttributeOverrides, as following:
@ElementCollection
@CollectionTable(name="HOMES") // Uses default join column name
@AttributeOverrides({
    @AttributeOverride(name="street",
        column=@Column(name="HOME_STREET")),
    @AttributeOverride(name="city",
        column=@Column(name="HOME_CITY")),
    @AttributeOverride(name="state",
        column=@Column(name="HOME_STATE"))
})
private Collection<Address> vacationHomes;
Si l'annotation @CollectionTable au @AttributeOverrides manque, des valeurs par défaut sont appliquées

Mapping de référence embarquée

Une référence embarquée contient des données qui sont stockées dans la même table du modèle relationnel que l'entité principale. Par exemple, si vous avez une entité Address (adresse) embarquée associée à une entité Customer (acheteur), les données de l'adresse seront enregistrées dans la même table que les données de l'acheteur. Comment réaliser la mise en correspondance avec JPA ? En utilisant l'annotation @AttributeOverrides de cette manière :
@Embedded
@AttributeOverrides({
 @AttributeOverride(name="street", column=@Column("ADDR_STREET")),
 @AttributeOverride(name="zip", column=@Column("ADDR_ZIP"))
 @AttributeOverride(name="city", column=@Column("ADDR_CITY")),
 @AttributeOverride(name="country", column=@Column("ADDR_COUNTRY"))
})
private Address address;
Si vous n'utilisez par @AttributeOverrides, des valeurs par défaut s'appliquent.

Conversion de type

La conversion de type entre Java et la base de donnée relationnelle est le travail de l'implémentation JPA (OpenXava utilise Hibernate par défaut). Normalement, la conversion par défaut est correcte pour la plupart des cas, mais si vous travaillez avec une base de donnée existante, vous auriez besoin des quelques astuces de cette section.
Etant donné que OpenXava utilise les facilités d'Hibernate pour les conversions de type, vous pouvez en apprendre plus sur ce sujet en lisant la documentation Hibernate.

Conversion de propriétés

Lorsque le type d'une propriété Java et le type de la colonne de la base de donnée relationnelle ne correspondent pas, vous devez écrire un type Hibernate (Hibernate Type) afin d'exécuter votre conversion personnalisée.
Par exemple, si vous avez une propriété de type String[] et que vous souhaitez enregistrer sa valeur en l'agrégeant dans une seule colonne de table de type VARCHAR, vous devez déclarer la conversion de votre propriété ainsi :
@Type(type="org.openxava.test.types.RegionsType")
private String [] regions;
La logique de conversion dans RegionsType est la suivante :
package org.openxava.test.types;
 
import java.io.*;
import java.sql.*;
 
import org.apache.commons.logging.*;
import org.hibernate.*;
import org.hibernate.usertype.*;
import org.openxava.util.*;
 
/**
 *
 * @author Javier Paniza
 */
 
public class RegionsType implements UserType { // 1
 
 public int[] sqlTypes() {
 return new int[] { Types.VARCHAR };
 }
 
 public Class returnedClass() {
 return String[].class;
 }
 
 public boolean equals(Object obj1, Object obj2) throws HibernateException {
 return Is.equal(obj1, obj2);
 }
 
 public int hashCode(Object obj) throws HibernateException {
 return obj.hashCode();
 }
 
 public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner) // 2
 throws HibernateException, SQLException
 {
 Object o = resultSet.getObject(names[0]);
 if (o == null) return new String[0];
 String dbValue = (String) o;
 String [] javaValue = new String [dbValue.length()];
 for (int i = 0; i < javaValue.length; i++) {
 javaValue[i] = String.valueOf(dbValue.charAt(i));
 }
 return javaValue;
 }
 
 public void nullSafeSet(PreparedStatement ps, Object value, int index) // 3
 throws HibernateException, SQLException
 {
 if (value == null) {
 ps.setString(index, "");
 return;
 }
 String [] javaValue = (String []) value;
 StringBuffer dbValue = new StringBuffer();
 for (int i = 0; i < javaValue.length; i++) {
 dbValue.append(javaValue[i]);
 }
 ps.setString(index, dbValue.toString());
 }
 
 public Object deepCopy(Object obj) throws HibernateException {
 return obj == null?null:((String []) obj).clone();
 }
 
 public boolean isMutable() {
 return true;
 }
 
 public Serializable disassemble(Object obj) throws HibernateException {
 return (Serializable) obj;
 }
 
 public Object assemble(Serializable cached, Object owner) throws HibernateException {
 return cached;
 }
 
 public Object replace(Object original, Object target, Object owner) throws HibernateException {
 return original;
 }
 
}
Le convertisseur de type doit implémenter l'interface org.hibernate.usertype.UserType(1). Les principales méthodes sont nullSafeGet() (2) pour lire la valeur de la base de donnée et la convertir en Java et nullSafeSet() (3) pour écrire la valeur Java dans la base de données.
OpenXava offre des type génériques de convertisseurs dans le paquet org.openxava.types prêts à l'emploi. L'un de ceux-là est le convertisseur EnumLetterType qui permet de mettre en correspondance des type enum. Par exemple, si vous avez une propriété telle que celle-ci :
private Distance distance;
public enum Distance { LOCAL, NATIONAL, INTERNATIONAL };
Dans cette propriété Java, 'LOCAL' vaut 1, 'NATIONAL' vaut 2 et 'INTERNATIONAL' vaut 3 lorsque la propriété est enregistrée dans la base de données. Mais que se passe-t-il si dans la base de données, une lettre doit être enregistrée ? Dans ce cas, vous pouvez utiliser EnumLetterType de cette façon :
@Type(type="org.openxava.types.EnumLetterType",
 parameters={
 @Parameter(name="letters", value="LNI"),
 @Parameter(name="enumType", value="org.openxava.test.model.Delivery$Distance")
 }
)
private Distance distance;
public enum Distance { LOCAL, NATIONAL, INTERNATIONAL };
Puisque vous spécifiez 'LNI' comme valeurs de l'attribut letters, le convertisseur de type fait correspondre 'L' à 1, 'N' à 2 et 'I' à 3. Remarquez aussi comment des convertisseurs peuvent être configurés en utilisant leurs propriétés ce qui les rend plus réutilisables.

Conversion de colonnes multiples

Avec l'annotation @CompositeUserType, vous pouvez faire correspondre plusieurs colonnes de table à une seule propriété Java. Ceci peut être utile si vous avez des propriétés définies par des classes personnalisées qui ont elles-mêmes plusieurs propriétés à enregistrer. Cette annotation est aussi utilisée lorsque vous travaillez avec une base de données existante.
Une exemple type est les convertisseur générique Date3Type qui permet de stocker dans 3 colonnes de la base de données une seule propriété Java avec le type java.util.Date.
@Type(type="org.openxava.types.Date3Type")
@Columns(columns = {
 @Column(name="YEARDELIVERY"),
 @Column(name="MONTHDELIVERY"),
 @Column(name="DAYDELIVERY")
})
private java.util.Date dateLivraison;
DAYDELIVERY, MONTHDELIVERY et YEARDELIVERY sont trois colonnes de la base de données qui enregistrent la valeur de la date de livraison. Voici le code source de Date3Type :
package org.openxava.types;
 
import java.io.*;
import java.sql.*;
 
import org.hibernate.*;
import org.hibernate.engine.*;
import org.hibernate.type.*;
import org.hibernate.usertype.*;
import org.openxava.util.*;
 
/**
 * Dans le code java, un <tt>java.util.Date</tt> et dans la base de
 * données, 3 colonnes de type entier (integer). <p>
 *
 * @author Javier Paniza
 */
 
public class Date3Type implements CompositeUserType { // 1
 
 public String[] getPropertyNames() {
 return new String[] { "year", "month", "day" };
 }
 
 public Type[] getPropertyTypes() {
 return new Type[] { Hibernate.INTEGER, Hibernate.INTEGER, Hibernate.INTEGER };
 }
 
 public Object getPropertyValue(Object component, int property) throws HibernateException { // 2
 java.util.Date date = (java.util.Date) component;
 switch (property) {
 case 0:
 return Dates.getYear(date);
 case 1:
 return Dates.getMonth(date);
 case 2:
 return Dates.getYear(date);
 }
 throw new HibernateException(XavaResources.getString("date3_type_only_3_properties"));
 }
 
 public void setPropertyValue(Object component, int property, Object value)
 throws HibernateException // 3
 {
 java.util.Date date = (java.util.Date) component;
 int intValue = value == null?0:((Number) value).intValue();
 switch (property) {
 case 0:
 Dates.setYear(date, intValue);
 case 1:
 Dates.setMonth(date, intValue);
 case 2:
 Dates.setYear(date, intValue);
 }
 throw new HibernateException(XavaResources.getString("date3_type_only_3_properties"));
 }
 
 public Class returnedClass() {
 return java.util.Date.class;
 }
 
 public boolean equals(Object x, Object y) throws HibernateException {
 if (x==y) return true;
 if (x==null || y==null) return false;
 return !Dates.isDifferentDay((java.util.Date) x, (java.util.Date) y);
 }
 
 public int hashCode(Object x) throws HibernateException {
 return x.hashCode();
 }
 
 public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner)
 throws HibernateException, SQLException // 4
 {
 Number year = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[0] );
 Number month = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[1] );
 Number day = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[2] );
 
 int iyear = year == null?0:year.intValue();
 int imonth = month == null?0:month.intValue();
 int iday = day == null?0:day.intValue();
 
 return Dates.create(iday, imonth, iyear);
 }
 
 public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session)
 throws HibernateException, SQLException // 5
 {
 java.util.Date d = (java.util.Date) value;
 Hibernate.INTEGER.nullSafeSet(st, Dates.getYear(d), index);
 Hibernate.INTEGER.nullSafeSet(st, Dates.getMonth(d), index + 1);
 Hibernate.INTEGER.nullSafeSet(st, Dates.getDay(d), index + 2);
 }
 
 public Object deepCopy(Object value) throws HibernateException {
 java.util.Date d = (java.util.Date) value;
 if (value == null) return null;
 return (java.util.Date) d.clone();
 }
 
 public boolean isMutable() {
 return true;
 }
 
 public Serializable disassemble(Object value, SessionImplementor session)
 throws HibernateException
 {
 return (Serializable) deepCopy(value);
 }
 
 public Object assemble(Serializable cached, SessionImplementor session, Object owner)
 throws HibernateException
 {
 return deepCopy(cached);
 }
 
 public Object replace(Object original, Object target, SessionImplementor session, Object owner)
 throws HibernateException
 {
 return deepCopy(original);
 }
 
}
Le convertisseur implémente l'interface CompositeUserType (1). Les méthodes clés sont getPropertyValue() (2) et setPropertyValue() (3) pour récupérer respectivement définir les valeurs des propriétés de l'objet et nullSafeGet() (4) et nullSafeSet() (5) pour lire et enregistrer l'objet de et dans la base de données.

Conversion de références

La conversion de références n'est pas pris en charge directement par Hibernate. Dans certaines très rares circonstances, vous devrez peut-être faire appel à une conversion de référence. Cette section vous explique comment faire.
Par exemple, vous pourriez avoir une référence vers une entité permis de conduire (driving licence) qui utilise deux colonnes DRIVINGLICENCE_LEVEL et DRIVINGLICENCE_TYPE et cette dernière colonne n'accepte pas de valeurs nulles , mais il est quand même possible que l'objet père puisse ne pas avoir de références vers une entité permis de conduire. Dans ce cas, il faut que la colonne DRIVINGLICENCE_TYPE contienne une chaîne de caractères vide. Ceci n'est pas un cas normal si vous cconcevez une base de données en utilisant des clés étrangères, mais si elle a été conçue par un développeur RPG, c'était la manière de faire, car les développeurs RPG ne savaient pas travailler avec des valeurs nulles.
Cela signifie que vous avez besoin d'un convertisseur pour la colonne DRIVINGLICENCE_TYPE pour transformer null en chaîne de caractère vide. Ceci peut être résolu avec un convertisseur comme celui-ci :
// Nous appliquons la conversion (null vers chaîne de caractères vide) à la colonne
// DRIVINGLICENCE_TYPE.
// Afin d'y arriver, nous créons drivingLicence_level et drivingLicence_type,
// nous rendons les annotations @JoinColumns ni insérables ni modifiables,
// nous modifions les méthodes get/setDrivingLicence, puis nous créons la méthode
// drivingLicenceConversion().
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumns({ // 1
 @JoinColumn(name="DRIVINGLICENCE_LEVEL", referencedColumnName="LEVEL",
 insertable=false, updatable=false),
 @JoinColumn(name="DRIVINGLICENCE_TYPE", referencedColumnName="TYPE",
 insertable=false, updatable=false)
})
private DrivingLicence drivingLicence;
private Integer drivingLicence_level; // 2
private String drivingLicence_type; // 2
 
public DrivingLicence getDrivingLicence() { // 3
 // De cette manière parce que la colonne du type de permis de conduire n'admet
 // pas les valeurs nulles
 try {
 if (drivingLicence != null) drivingLicence.toString(); // to force load
 return drivingLicence;
 }
 catch (EntityNotFoundException ex) {
 return null;
 }
}
 
public void setDrivingLicence(DrivingLicence licence) { // 4
 // De cette manière parce que la colonne du type de permis de conduire n'admet
 // pas les valeurs nulles
 this.drivingLicence = licence;
 this.drivingLicence_level = licence==null?null:licence.getLevel();
 this.drivingLicence_type = licence==null?null:licence.getType();
}
 
@PrePersist @PreUpdate
private void drivingLicenceConversion() { // 5
 if (this.drivingLicence_type == null) this.drivingLicence_type = "";
}
 
Premièrement, vous devez utiliser les annotations @JoinColumns avec les attributs insertable=false et updateable=false pour toutes les annotations @JoinColumn (1) de manière à ce que la référence soit lue depuis la base de données, mais pas écrite. Deuxièmement, définissez des propriétés standards pour les clés étrangères de la référence (2). Troisièmement, écrivez un getter, getDrivingLicence() (3), pour retourner null lorsque la référence n'est pas trouvée. Et quatrièmement, définissez un setter, setDrivingLicence() (4) pour assigner la clé de la référence aux propriétés correspondant aux clés étrangères.
Cinquièmement, et finalement, vous devez écrire une méthode de rappel, drivingLicenceConversion() (5), pour effectuer le travail de conversion. Cette méthode sera exécuté automatiquement lors des créations et des mises à jour.
Cette exemple montre comment l'on peut englober des base de données existantes avec un peu de programmation et quelques ressources de base de JPA.